<template>
  <div
    class="cnhis-drag-container"
    @mousedown="containerMousedown"
    :style="containerStyles"
  >
    <slot></slot>
    <div
      v-if="markLine.x !== null"
      class="line-x"
      :style="{ left: markLine.x + 'px' }"
    ></div>
    <div
      v-if="markLine.y !== null"
      class="line-y"
      :style="{ top: markLine.y + 'px' }"
    ></div>
  </div>
</template>

<script>
import mitt from "mitt";
const events = mitt();

export default {
  data() {
    return {
      blocks: [],
      selectIndex: -1,
      dragState: {
        startX: 0,
        startY: 0,
        dragging: false, // 默认不是正在拖拽
      },
      markLine: {
        x: null,
        y: null,
      },
      behaviour: {
        queue: [],
        current: -1,
        commandArray: [],
        commands: {},
      },
      keyboardEvent: () => {},
    };
  },
  props: {
    width: {
      type: String,
      default: "100%",
    },
    height: {
      type: String,
      default: "100%",
    },
    value: {
      type: Array,
      default: () => {},
    },
  },
  computed: {
    containerStyles() {
      return {
        width: this.width,
        height: this.height,
      };
    },
    focusData() {
      const focus = [];
      const unfocused = [];
      this.blocks.forEach((block) =>
        (block.focus ? focus : unfocused).push(block)
      );
      console.log({
        focus,
        unfocused,
      })
      return {
        focus,
        unfocused,
      };
    },
    lastSelectBlock() {
      return this.blocks[this.selectIndex];
    },
  },
  methods: {
    handleBlockMousedown(e, vnode) {
      e.preventDefault();
      e.stopPropagation();
      let data = null;
      if (vnode.componentInstance) {
        data = vnode.componentOptions.propsData.data;
      } else {
        data = vnode.data.attrs.data;
      }
      const curBlockIndex = this.blocks.findIndex((i) => i.id === data.id);
      const curBlock = this.blocks[curBlockIndex];
      if (e.shiftKey) {
        if (this.focusData.focus.length <= 1) {
          this.$set(curBlock, "focus", true);
          vnode.elm.classList.add("editor-block-focus");
        } else {
          this.$set(curBlock, "focus", !curBlock.focus);
          if (curBlock.focus) {
            vnode.elm.classList.add("editor-block-focus");
          } else {
            vnode.elm.classList.remove("editor-block-focus");
          }
        }
      } else {
        if (!curBlock.focus) {
          this.clearBlockFocus();
          this.$set(curBlock, "focus", true);
          vnode.elm.classList.add("editor-block-focus");
        }
      }
      this.$set(this.blocks, curBlockIndex, curBlock);
      this.selectIndex = curBlockIndex;
      this.handleBlockMove(e);
    },
    handleBlockMove(e) {
      const { offsetWidth: BWidth, offsetHeight: BHeight } =
        this.lastSelectBlock.vnode.elm;
      const { left, top } = this.lastSelectBlock;
      this.dragState = {
        startX: e.clientX,
        startY: e.clientY,
        startLeft: left,
        startTop: top,
        dragging: false,
        startPos: this.focusData.focus.map(({ top, left }) => ({ top, left })),
        lines: (() => {
          const { unfocused } = this.focusData;
          let lines = { x: [], y: [] };
          [...unfocused].forEach((block) => {
            const { top: ATop, left: ALeft } = block;
            const AWidth = block.vnode.elm.offsetWidth;
            const AHeight = block.vnode.elm.offsetHeight;
            // 当此元素拖拽到和A元素top一致的时候，要显示这根辅助线，辅助线的位置就是ATop
            lines.y.push({ showTop: ATop, top: ATop });
            lines.y.push({ showTop: ATop, top: ATop - BHeight }); // 顶对底
            lines.y.push({
              showTop: ATop + AHeight / 2,
              top: ATop + AHeight / 2 - BHeight / 2,
            }); // 中对中
            lines.y.push({ showTop: ATop + AHeight, top: ATop + AHeight }); // 底对顶
            lines.y.push({
              showTop: ATop + AHeight,
              top: ATop + AHeight - BHeight,
            }); // 底对底

            lines.x.push({ showLeft: ALeft, left: ALeft }); // 左对左边
            lines.x.push({ showLeft: ALeft + AWidth, left: ALeft + AWidth }); // 右边对左边
            lines.x.push({
              showLeft: ALeft + AWidth / 2,
              left: ALeft + AWidth / 2 - BWidth / 2,
            });
            lines.x.push({
              showLeft: ALeft + AWidth,
              left: ALeft + AWidth - BWidth,
            });
            lines.x.push({ showLeft: ALeft, left: ALeft - BWidth }); // 左对右
          });

          return lines;
        })(),
      };
      // events.emit("start");
      document.addEventListener("mousemove", this.mousemove);
      document.addEventListener("mouseup", this.mouseup);
    },
    mousemove(e) {
      let { clientX: moveX, clientY: moveY } = e;
      if (!this.dragState.dragging) {
        this.dragState.dragging = true;
      }

      const left = moveX - this.dragState.startX + this.dragState.startLeft;
      const top = moveY - this.dragState.startY + this.dragState.startTop;
      let y = null;
      let x = null;
      for (let i = 0; i < this.dragState.lines.y.length; i++) {
        const { top: t, showTop: s } = this.dragState.lines.y[i];
        if (Math.abs(t - top) < 5) {
          y = s;
          moveY = this.dragState.startY - this.dragState.startTop + t;
          break;
        }
      }

      for (let i = 0; i < this.dragState.lines.x.length; i++) {
        const { left: l, showLeft: s } = this.dragState.lines.x[i];
        if (Math.abs(l - left) < 5) {
          x = s;
          moveX = this.dragState.startX - this.dragState.startLeft + l;
          break;
        }
      }
      this.markLine.x = x;
      this.markLine.y = y;

      const durX = moveX - this.dragState.startX;
      const durY = moveY - this.dragState.startY;

      this.focusData.focus.forEach((block, idx) => {
        this.$set(block, "top", this.dragState.startPos[idx].top + durY);
        this.$set(block, "left", this.dragState.startPos[idx].left + durX);
      });
      this.init();
    },
    mouseup() {
      document.removeEventListener("mousemove", this.mousemove);
      document.removeEventListener("mouseup", this.mouseup);
      this.markLine.x = null;
      this.markLine.y = null;
      // this.$emit("input", this._.cloneDeep(this.dealBlocksToValues()));
      events.emit("end");
    },
    dealBlocksToValues() {
      return this.blocks.map((i) => {
        const obj = { ...i };
        delete obj.focus;
        delete obj.vnode;
        return obj;
      });
    },
    containerMousedown() {
      this.clearBlockFocus();
      this.selectIndex = -1;
    },
    clearBlockFocus() {
      this.blocks.forEach((block) => {
        block.vnode.elm.classList.remove("editor-block-focus");
        this.$set(block, "focus", false);
      });
    },
    dealBlocks(datas) {
      return datas.map((i) => {
        this.$set(i, "focus", false);
        return i;
      });
    },
    init() {
      this.$nextTick(() => {
        const slotVnodes = this.$slots.default || [];
        for (let i = 0; i < this.blocks.length; i++) {
          const vnode = slotVnodes[i];
          const el = vnode.elm;
          this.$set(this.blocks[i], "vnode", vnode);
          // 绑定样式
          // el.style.position = "absolute";
          // el.style.top = this.blocks[i].top + "px";
          // el.style.left = this.blocks[i].left + "px";
          el.style.webkitTransform = `translate3d(${ this.blocks[i].left + "px"},${this.blocks[i].top + "px"}, 1px)`
          el.classList.add("editor-block");
          el.addEventListener("mousedown", (e) =>
            this.handleBlockMousedown(e, vnode)
          );
        }
      });
    },
    dragBefore() {
      events.emit("start");
    },
    dragAfter() {
      events.emit("end");
    },
    registry(command) {
      this.behaviour.commandArray.push(command);
      this.behaviour.commands[command.name] = (...args) => {
        const { redo, undo } = command.execute(...args);
        redo();
        if (!command.pushQueue) {
          // 不需要放到队列中直接跳过即可
          return;
        }
        let { queue, current } = this.behaviour;
        if (queue.length > 0) {
          queue = queue.slice(0, current + 1); // 可能在放置的过程中有撤销操作，所以根据当前最新的current值来计算新的对了
          this.behaviour.queue = queue;
        }
        this.behaviour.current = current + 1;

        this.behaviour.queue.push({ redo, undo }); // 保存指令的前进后退
      };
    },
    registryCommands() {
      const _this = this;
      _this.registry({
        name: "redo",
        keyboard: "ctrl+y",
        execute() {
          return {
            redo() {
              let item = _this.behaviour.queue[_this.behaviour.current + 1]; // 找到当前的下一步还原操作
              if (item) {
                item.redo && item.redo();
                _this.behaviour.current++;
              }
            },
          };
        },
      });
      _this.registry({
        name: "undo",
        keyboard: "ctrl+z",
        execute() {
          return {
            redo() {
              if (_this.behaviour.current == -1) return; // 没有可以撤销的了
              let item = _this.behaviour.queue[_this.behaviour.current]; // 找到上一步还原
              if (item) {
                item.undo && item.undo(); // 这里没有操作队列
                _this.behaviour.current--;
              }
            },
          };
        },
      });
      _this.registry({
        name: "drag",
        pushQueue: true,
        init() {
          const start = () => {
            this.before = _this._.cloneDeep(_this.dealBlocksToValues());
          };
          const end = () => {
            _this.behaviour.commands.drag();
          };
          events.on("start", start);
          events.on("end", end);
        },
        execute() {
          const before = this.before;
          const after = _this.dealBlocksToValues();
          return {
            redo() {
              _this.$emit("input", _this._.cloneDeep(after, true));
            },
            undo() {
              _this.$emit("input", before);
            }
          };
        },
      });

      this.keyboardEvent = (() => {
        const keyCodes = {
          90: "z",
          89: "y",
        };
        const onKeydown = (e) => {
          const { ctrlKey, keyCode } = e; // ctrl+z  / ctrl+y
          let keyString = [];
          if (ctrlKey) keyString.push("ctrl");
          keyString.push(keyCodes[keyCode]);
          keyString = keyString.join("+");

          _this.behaviour.commandArray.forEach(({ keyboard, name }) => {
            if (!keyboard) return; // 没有键盘事件
            if (keyboard === keyString) {
              _this.behaviour.commands[name]();
              e.preventDefault();
            }
          });
        };
        const init = () => {
          // 初始化事件
          window.addEventListener("keydown", onKeydown);
          return () => {
            // 销毁事件
            window.removeEventListener("keydown", onKeydown);
          };
        };
        return init;
      })();
      this.keyboardEvent();

      this.behaviour.commandArray.forEach(
        (command) => command.init && command.init()
      );
    },
  },
  mounted() {
    this.registryCommands();
  },
  watch: {
    value: {
      handler(val) {
        if (!val.length) return;
        this.blocks = this.dealBlocks(this._.cloneDeep(val));
        this.init();
      },
      deep: true,
      immediate: true,
    },
  },
};
</script>

<style lang="less">
.cnhis-drag-container {
  background: #f1f1f1;
  position: relative;
  .line-x {
    position: absolute;
    top: 0;
    bottom: 0;
    border-left: 1px dashed red;
  }
  .line-y {
    position: absolute;
    left: 0;
    right: 0;
    border-top: 1px dashed red;
  }
  .editor-block {
    position: absolute;
    will-change: transform;
    &::after {
      content: "";
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
    }
  }
  .editor-block-focus {
    &::after {
      border: 3px dashed red;
    }
  }
}
</style>